JVM 的基本操作指令介绍及其程序运行原理

您所在的位置:网站首页 binary to text generator JVM 的基本操作指令介绍及其程序运行原理

JVM 的基本操作指令介绍及其程序运行原理

2022-05-22 04:24| 来源: 网络整理| 查看: 265

java虚拟机是对实体计算机的模拟,任何用高级语言开发的程序,必须编译成二进制代码后,才能在CPU上运行,而这些二进制代码本质上是一系列基础指令集的集合,这些指令的作用非常简单,例如把数值存到某块内存,或者把信息从某地读出,然后放入到寄存器中。

同理,运行在java虚拟机上的字节码,其本质也是一系列作用单一的指令的集合。我们看看,要完成一个简单的计算功能,例如计算 6 * 9 的结果,虚拟机是如何依赖基础指令来完成相应计算功能的。计算式 6 * 9 被编译成java字节码后,对应的指令如下:

bipush  6 bipush  9 imul

我们前面强调过,jvm是以栈为基础来运行指令的,任何指令的执行,如果需要参数的话,这些参数必须先压入虚拟机的堆栈上,指令才可以正常执行。从上面的代码可以看成,指令imul 的作用是做乘法操作,要相乘的两个数在执行这句指令前,必须都压入到堆栈的顶部,因此指令bipush 6的作用是把数值6压入到堆栈顶部,bipush 9作用是把数值9压入到堆栈顶部,执行这两条指令后堆栈情况如下:stack: 9, 6

当虚拟机执行指令imul后,会把堆栈顶部的两个数值弹出,把他们相乘,然后把乘积压入堆栈顶部,所以imul指令执行后,堆栈情况如下:stack: 54

java虚拟机的指令有一些特点,在指令的前面会有一些字符用于表示指令所要操作的数据类型,例如bipush指令,push前面的字符i表示指令只接受整形数据,i的意思是int, 最前面的b表示byte, 也就是说bipush指令只能操作范围在-128到128之间的整数,同理如果指令是sipush,表示该指令只能把short类型的整数压入到堆栈。同理指令imul表示它只能处理两个整形数相乘,如果此时堆栈顶部的两个数字不是整形的话,那么执行imul指令时,虚拟机就会报错。以下我们给出字符对应的数据类型:

Letter Type a reference 指令处理的是对象引用 b byte or boolean c char d double i int l long s short

如果你看到指令fmul, 你就能知道,这条指令的作用是对两个浮点数做乘法。相应指令的具体作用,大家可以查询java虚拟机规范,这里我们不深入探讨。

我们也提到过,java虚拟机的运行环境,不单单有堆栈,它还有一个局部变量队列,用来存储临时变量的信息。因此java虚拟机也提供了相应的操作指令。假设虚拟机堆栈和局部变量队列的数据存储情况如下:

stack: 1array: _ _ _ 2 

也就是堆栈顶部数据为1,在具备变量队列中,下标为3的地方存储了一个整形2,如果执行了指令 istore 3, 那么虚拟机就会把局部变量队列下标为3处所存储的整形数压入堆栈顶部,于是执行完指令后,堆栈和队列的情况如下:

stack: 2, 1array: 

如果接着执行的指令是 iload 2, 它的意思是,把堆栈顶部的数值弹出,然后存储到局部变量队列下标为2的区域处,于是执行完后,虚拟机的堆栈和队列情况如下:

stack: 1array: _ _ 2

同理load和store前面的字符用于表明指令所能处理的数据,如果想把一个浮点数从队列压入到堆栈,那么对应的指令就是 fstroe . 假设局部变量队列在下标2为1 的地方存储的是对一个对象的引用,要想把这个引用压入到堆栈顶部,对应的指令就是 astore 1. store 前面的a表示的就是对象引用。这里我们只需要理解指令的基本原理,具体的相关指令,大家可以通过查询java虚拟机规范来了解。

有了上面的基础知识后,我们就可以看看,如何将下面的C语言代码编译成java字节码:

void f() {   printf("The result of 3 multiply 6 is :%d", 3*6); } void main() {   f(); }

我们的编译器在解析上面代码时,读取到数字字符是,会根据表达式:Unary -> NUMBER进行解析,因此当编译器在执行上面表达式的解析时,我们就知道当前已经读取到了数字常量,此时我们可以通过push指令把该数字常量压入虚拟机堆栈上。解析上面表达式的代码是在UnaryExecutor中,所以在UnaryExecutor.java中,我们做如下修改:

public class UnaryNodeExecutor extends BaseExecutor implements IExecutorReceiver{ ....  public Object Execute(ICodeNode root) {    ....    case CGrammarInitializer.Number_TO_Unary:            text = (String)root.getAttribute(ICodeKey.TEXT);            boolean isFloat = text.indexOf('.') != -1;            if (isFloat) {                value = Float.valueOf(text);                root.setAttribute(ICodeKey.VALUE, Float.valueOf(text));                } else {                value = Integer.valueOf(text);                root.setAttribute(ICodeKey.VALUE, Integer.valueOf(text));            }            ProgramGenerator.getInstance().emit(Instruction.IPUSH, value.toString());            break;    ....  } ....

在编译器读入代码,解析到一个数字常量时,我们输出push 指令,后面跟着对应的数字常量,所以当我们的编译器解析代码时,读取到数字字符3,那么上面的代码被执行,于是语句:ProgramGenerator.getInstance().emit(Instruction.IPUSH, value.toString());就会在java汇编文件中输出一条语句:sipush 3

同理,当读取字符6时,上面的代码又会被执行,于是在java汇编文件中又再输出一条语句:sipush 6

当表达式3 * 6 被解析时,由于该算式对应的语法表达式是:Binary -> Binary * Binary

所以,当我们的编译器执行上面的语法表达式时,我们就可以输出乘法指令imul. 执行上面表达式的解析是在BinaryExecutor.java中,所以代码改动如下:

public class BinaryExecutor extends BaseExecutor{    @Override    public Object Execute(ICodeNode root) {    ....    case CGrammarInitializer.Binary_Plus_Binary_TO_Binary:        case CGrammarInitializer.Binary_DivOp_Binary_TO_Binary:        case CGrammarInitializer.Binary_Minus_Binary_TO_Binary:        case CGrammarInitializer.Binary_Start_Binary_TO_Binary:        ......         else if (production ==  CGrammarInitializer.Binary_Start_Binary_TO_Binary) {                String text = root.getChildren().get(0).getAttribute(ICodeKey.TEXT) + " * " + root.getChildren().get(1).getAttribute(ICodeKey.TEXT);                root.setAttribute(ICodeKey.VALUE, val1 * val2);                    root.setAttribute(ICodeKey.TEXT,  text);                System.out.println(text + " is " + (val1 * val2) );                ProgramGenerator.getInstance().emit(Instruction.IMUL);            }            else {            ....            ....            }            break;    ....    }

大家可以看到,在解析两数相乘时,编译器通过代码:ProgramGenerator.getInstance().emit(Instruction.IMUL);在java汇编文件中输出指令 imul,到此我们编译器成功输出了以下语句:

sipush  3 sipush  6 imul

上面三条指令执行后,虚拟机的堆栈顶部存有的数值是18.接下来我们需要把数值18打印出来。通过上节我们了解到,要把信息输出到控制台上时,需要调用对象System.out 的 print 函数,也就是需要把System.out对象先压到堆栈上,再把要输出的内容压到堆栈上,也就是所,如果我们想要把结果18打印到控制台上,那么堆栈上的内存必须是这样:

stack: 18, System.out

但现在问题是,数值18处于堆栈顶部,要想实现上面所需的堆栈数据排列,我们需要把数值18先拿开,然后把对象System.out压入堆栈,接着再把数值18重新压入堆栈,因此我们需要先使用istore指令,把数值18从堆栈放入局部变量队列,也就是先执行指令istore 0, 使得虚拟机堆栈和队列的数据存储情况如下:

stack:array: 18

然后 执行指令getstatic ,把 System.out对象压入堆栈:

stack: System.outarray: 18

最后使用指令 iload 0 , 把位于队列偏移为0处的数值18重新加载会堆栈,执行完指令iload 0 后,虚拟机堆栈和队列上的内容如下:stack: 18, System.outarray: 

此时要输出到控制台的内容和执行输出操作的System.out对象都已经在堆栈顶部,我们再通过指令invokevirtual 调用System.out对象的Print接口就可以实现把数值18输出到控制台上的目的了。

因此编译器在把语句 printf(“The result of 3 multiply 6 is :%d”, 36); 转换成java字节码时,分以下几步走,首先把字符串”The result of 3 multiply 6 is “ 输出到控制台,接着解析表达式 3* 6,生成上头所说的三条java指令,然后再执行上头所说的步骤,把数值18和类对象System.out放入到堆栈顶部,进而再把数值18输出到控制台上。

这里的逻辑有点繁琐,请大家通过视频获得更清楚的讲解和演示。

上头所说的操作都需要在编译器解析printf调用时去具体处理,因此相关代码的实现在ClibCall.java 中,代码如下:

private Object handlePrintfCall() {        ArrayList argsList = FunctionArgumentList.getFunctionArgumentList().getFuncArgList(false);        String argStr = (String)argsList.get(0);        String formatStr = "";        int i = 0;        int argCount = 1;        while (i < argStr.length()) {            if (argStr.charAt(i) == '%' && i+1 < argStr.length() &&                    argStr.charAt(i+1) == 'd') {                i += 2;                generateJavaAssemblyForPrintf(formatStr);                formatStr += argsList.get(argCount);                argCount++;                printInteger();            } else {                formatStr += argStr.charAt(i);                i++;            }        }        System.out.println(formatStr);        generateJavaAssemblyForPrintf("\n");        return null;    }    private void generateJavaAssemblyForPrintf(String s) {        ProgramGenerator generator = ProgramGenerator.getInstance();        generator.emit(Instruction.GETSTATIC, "java/lang/System/out Ljava/io/PrintStream;");        generator.emit(Instruction.LDC, "\"" + s + "\"");        String printMethod = "java/io/PrintStream/print(Ljava/lang/String;)V";        generator.emit(Instruction.INVOKEVIRTUAL, printMethod);    }    private void printInteger() {        ProgramGenerator generator = ProgramGenerator.getInstance();        generator.emit(Instruction.ISTORE, "0");        generator.emit(Instruction.GETSTATIC, "java/lang/System/out Ljava/io/PrintStream;");        generator.emit(Instruction.ILOAD, "0");        String printMethod = "java/io/PrintStream/print(I)V";        generator.emit(Instruction.INVOKEVIRTUAL, printMethod);    }

在handlePrintfCall调用中,在输出数值之前,先通过generateJavaAssemblyForPrintf调用,把字符串先输出,然后调用printInteger接着把数值输出到控制台上,在printInteger中,先把指令istore 0 输出到Java汇编文件里,然后输出指令getstatic , 目的就是把System.out对象压入到堆栈上,然后再次通过输出指令 iload 0 , 把数值18重新压入到堆栈顶部,最后在通过invokevirtual指令,把数值18输出到控制台。

上面的代码执行后,C语言代码被编译成如下java汇编代码:

.class public CSourceToJava .super java/lang/Object .method public static main([Ljava/lang/String;)V    invokestatic    CSourceToJava/f()V    return .end method .method public static f()V    sipush    3    sipush    6    imul    getstatic    java/lang/System/out Ljava/io/PrintStream;    ldc    "The result of 3 multiply 6 is :"    invokevirtual    java/io/PrintStream/print(Ljava/lang/String;)V    istore    0    getstatic    java/lang/System/out Ljava/io/PrintStream;    iload    0    invokevirtual    java/io/PrintStream/print(I)V    return .end method .end class

我们可以看到,代码先是通过三条指令sipush 3sipush 6imul计算 3*6 , 接着把字符串”The result of 3 multiply 6 is :”压到堆栈打印出来,最后再通过前面所说的指令操作,把堆栈顶部的数值18打印到控制台上。

请大家通过视频获得更详实的讲解和更清晰的调试演示过程。

更多技术信息,包括操作系统,编译器,面试算法,机器学习,人工智能,请关照我的公众号:



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3